Redux 提供的主要功能:全局数据管理,包括数据的更新、存储、数据变化通知。 Redux 的 store 中存放了当前应用的状态,可以根据这个状态完整恢复出当前应用的界面,因此在使用 Redux 的项目中,可以实现一个比较炫酷的功能:依据状态的前进、后退。
Redux 中主要有三大块:
- Action:指代引起 Redux 中数据变化的操作;
- Reducer:响应 Action 操作,修改 Redux 中的数据;
- Store:包含一个 state 对象,用于存放整个应用的数据,并整合将 Action 和 Reducer 整合起来,修改 Store 中的数据。
目前,网上已经有很多中文资料介绍具体概念细节以及相关 API 了,比如:
这里主要想记录一下作为一个初学 Redux 的菜鸟,使用过程中的心得体会。
单单就 Redux 本身来看,并不能直接用于生产,太灵活了,有很多“套路”需要强制定下来:
- 怎么设计 state 对象的数据结构?怎么分好模块?怎么规定各个模块的命名风格?
- Redux 只提供了注册 state 变化回调函数的 API ,如果只想监听其中某一个数据的变化该怎么办?
- 如果在 state change 的回调函数中再次 dispatch Action ,就可能造成无限递归,怎么设计才能很好地避免这种无限递归?
- 如何设计组织项目代码才更好维护?
- 如何避免写大量重复的 Action 、 Reducer 代码?
划分代码目录
目录的划分方式有多种,可以按照项目的功能模块,也可以按照 Redux 的职责模块。我选择了后者,采用的目录结构如下:
外部可以直接引入的 JS 模块只能是 data/main
和 data/actionTypes
。
解决递归调用
为什么会有递归调用呢?参考如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import {createStore} from 'redux'; function reducer(state = {}, action) { switch (action.type) { case 'SOME_THING_LOAD_COMPLETE': return Object.assign({}, state, { loadComplete: true }); default: return state; } } let store = createStore(reducer); store.subscribe(() => store.dispatch({type: 'SOME_THING_LOAD_COMPLETE'}); ); store.dispatch({type: 'SOME_THING_LOAD_COMPLETE'});
|
上面这个简单的例子很清晰地说明了无限递归的问题,在实际开发中,由于业务逻辑的复杂纠缠,这个递归过程可能非常间接、隐蔽,造成 debug 困难。那么如何有效避免呢?
一个比较常用的方法就是检查对应数据是否真的发生了变化,比如上面的代码可以改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import {createStore} from 'redux'; function reducer(state = {}, action) { switch (action.type) { case 'SOME_THING_LOAD_COMPLETE': return Object.assign({}, state, { loadComplete: true }); default: return state; } } let store = createStore(reducer); let previousLoadComplete; store.subscribe(() => let currentLoadComplete = store.getState().loadComplete; if (currentLoadComplete !== previousLoadComplete) { store.dispatch({type: 'SOME_THING_LOAD_COMPLETE'}); previousLoadComplete = currentLoadComplete; } ); store.dispatch({type: 'SOME_THING_LOAD_COMPLETE'});
|
由于 state 每次更新都会在相应位置产生一个新对象,所以只需要用全等来判断就行了。
组织 state 数据结构
如何划分 state 对象的结构呢?可能每个人根据自己的经验,都有自己的一套划分方式。此处我采用了与业务功能模块对齐的原则。
比如,我的项目里面有这样一些页面:用户列表页面、用户详情页面、资源页面、资源详情页面,那么 state 对象的结构为:
1 2 3 4 5 6
| state = { 'user.list': { ... }, 'user.detail': { ... }, 'resource.list': { ... }, 'resource.detail': { ... } }
|
结构扁平化了。
个人建议,不要使用“多层”的 state 结构,比如把上面的例子设计成:
1 2 3 4 5 6 7 8 9 10 11
| state = { user: { list: { ... }, detail: { ... } }, resource: { list: { ... }, detail: { ... } } };
|
过深的结构会带来不必要的复杂度。
扩展事件监听方式
Redux 只提供了 subscribe
方法来监听 state 的变化,在实际开发中,某一个组件可能只对某部分 state 变化感兴趣。所以,应当适当地做一下扩展:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
| export default class StateWatcher { * 存放监听回调函数等 * * @private * @type {Array.<Object>} */ watcherList = []; * 构造函数 * * @constructor * @param {store} Store Redux store实例 */ constructor(store) { this.store = store; this.previousStoreState = extend({}, this.store.getState()); this.subscribe(); } * 订阅 store 中 state 变化事件,会去检查 watcherList 中是否存在相应数据变化的回调函数 * * @private */ subscribe() { let me = this; this.unsubscribe = this.store.subscribe(function () { let currentStoreState = me.store.getState(); let changedPropertyNameList = []; let delayFns = []; each(me.watcherList, watcher => { let propertyName = watcher.propertyName; let previous = me.previousStoreState[propertyName]; if (currentStoreState[propertyName] !== previous) { changedPropertyNameList.push(propertyName); if (!watcher.context.isInStage(componentState.DESTROIED)) { delayFns.push(() => { watcher.watcherFn.call( watcher.context, propertyName, currentStoreState[propertyName], previous ); }); } } }); each(changedPropertyNameList, propertyName => { me.previousStoreState[propertyName] = currentStoreState[propertyName]; }); each(delayFns, fn => fn()); }); } * 添加属性变化的回调函数 * * @public * @param {string} propertyName 属性名 * @param {Function} watcherFn 回调函数 * @param {Component} context 组件 */ addWatcher({propertyName, watcherFn, context}) { this.watcherList.push({propertyName, watcherFn, context}); } * 移除属性变化的回调函数 * * @public * @param {string} propertyName 属性名 * @param {Function} watcherFn 回调函数 * @param {Component} context 组件 */ removeWatcher({propertyName, watcherFn, context}) { this.watcherList = filter(this.watcherList, watcher => { return watcher.propertyName !== propertyName || watcher.watcherFn !== watcherFn || watcher.context !== context; }); } * 销毁 * * @public */ destroy() { this.unsubscribe(); } }
|
避免写重复代码
目前想到的,就只是抽离复用代码,形成 helper 方法之类的。
最后
本文所述方案仅供参考,算是我初次使用 Redux 所想到的一些“套路”,不对之处静候读者指出,共同探讨。